package com.smart.thrift.config.client;

import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.smart.thrift.annotation.ThriftReference;
import com.smart.thrift.annotation.ThriftService;
import com.smart.thrift.constants.ThriftConstants;
import com.smart.thrift.exception.client.ThriftClientInstantiateException;
import com.smart.thrift.model.common.ThriftServiceSignature;
import com.smart.thrift.thriftapi.BasicService;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.thrift.TServiceClient;
import org.apache.thrift.protocol.TProtocol;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RestController;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.springframework.context.annotation.AnnotationConfigUtils.registerAnnotationConfigProcessors;

@Slf4j
public final class ThriftRefClientBeanScanner extends ClassPathBeanDefinitionScanner {

    private final ApplicationContext applicationContext;

    private final Set<Wrapper> referenceClasses = new HashSet<>();

    public ThriftRefClientBeanScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters, ApplicationContext applicationContext,
                                      Environment environment, ResourceLoader resourceLoader) {
        super(registry, useDefaultFilters);
        setEnvironment(environment);
        setResourceLoader(resourceLoader);
        registerAnnotationConfigProcessors(registry);
        this.applicationContext = applicationContext;
    }

    public ThriftRefClientBeanScanner(BeanDefinitionRegistry registry, ApplicationContext applicationContext,
                                      Environment environment, ResourceLoader resourceLoader) {
        this(registry, true, applicationContext, environment, resourceLoader);
    }

    @Data
    @Builder
    public static class Wrapper {
        //被@ThriftReference注解的字段类型
        private Class<?> targetClass;
        //包含@ThriftReference注解的bean类型
        private Class<?> referenceClass;
        //被@ThriftReference注解的对象
        private AnnotatedElement element;
        //被@ThriftReference注解的对象名称
        private String elementName;
    }

    @Override
    protected void registerDefaultFilters() {
        this.addIncludeFilter(new AnnotationTypeFilter(ThriftService.class));
        this.addIncludeFilter(new AnnotationTypeFilter(RestController.class));
        super.registerDefaultFilters();
    }

    @Override
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {

        Set<BeanDefinitionHolder> definitionHolders = super.doScan(basePackages);
        log.info("Packages scanned by ThriftRefClientBeanScanner is [{}]", StringUtils.join(basePackages, ", "));

        for (Wrapper wrapper : referenceClasses) {
            if (!wrapper.getTargetClass().isInterface()) {
                continue;
            }
            ThriftReference thriftReference = AnnotationUtil.getAnnotation(wrapper.getElement(), ThriftReference.class);
            BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(ThriftClientFactoryBean.class);
            beanDefinitionBuilder.addPropertyValue(ThriftConstants.Definition.BEAN_NAME, thriftReference.name());
            beanDefinitionBuilder.addPropertyValue(ThriftConstants.Definition.BEAN_CLASS, wrapper.getTargetClass());
            beanDefinitionBuilder.addPropertyValue(ThriftConstants.Definition.BEAN_CLASS_NAME, wrapper.getTargetClass().getName());
            Class<? extends TServiceClient> clientClass;
            if (wrapper.getTargetClass().getName().endsWith("$Iface")) {
                clientClass = Stream.of(BasicService.Iface.class.getDeclaringClass().getDeclaredClasses())
                        .filter(clazz -> ClassUtil.isAssignable(TServiceClient.class, clazz))
                        .findFirst()
                        .map(client -> (Class<? extends TServiceClient>) client).orElse(null);
            } else {
                clientClass = BasicService.Client.class;
            }
            if (clientClass == null) {
                continue;
            }
            Constructor<? extends TServiceClient> constructor;
            try {
                constructor = clientClass.getConstructor(TProtocol.class);
            } catch (NoSuchMethodException e) {
                log.error(e.getMessage(), e);
                throw new ThriftClientInstantiateException("Failed to get constructor with args TProtocol", e);
            }
            beanDefinitionBuilder.addPropertyValue(ThriftConstants.Definition.CLIENT_CLASS, clientClass);
            beanDefinitionBuilder.addPropertyValue(ThriftConstants.Definition.CLIENT_CONSTRUCTOR, constructor);
            String name = StrUtil.isNotBlank(thriftReference.name()) ? thriftReference.name() : thriftReference.value();
            ThriftServiceSignature serviceSignature = ThriftServiceSignature.builder()
                    .serviceName(StrUtil.isNotBlank(name) ? name : wrapper.getElementName())
                    .targetServiceClass(wrapper.getTargetClass())
                    .thriftServiceClasses(wrapper.getTargetClass().isInterface() ? Collections.singletonList(wrapper.getTargetClass()) :Arrays.asList(wrapper.getTargetClass().getInterfaces()))
                    .version(thriftReference.version()).build();
            beanDefinitionBuilder.addPropertyValue(ThriftConstants.Definition.SERVICE_SIGNATURE, serviceSignature);
            beanDefinitionBuilder.addPropertyValue(ThriftConstants.Definition.APPLICATION_CONTEXT, applicationContext);
            beanDefinitionBuilder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
            beanDefinitionBuilder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
            AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
            assert getRegistry() != null;
            if (StrUtil.isNotBlank(thriftReference.name())) {
                getRegistry().registerBeanDefinition(thriftReference.name(), beanDefinition);
            } else {
                BeanDefinitionReaderUtils.registerWithGeneratedName(beanDefinition, getRegistry());
            }
        }
        return definitionHolders;
    }

    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        AnnotationMetadata metadata = beanDefinition.getMetadata();
        boolean contains = metadata.hasAnnotation(ThriftService.class.getName()) || metadata.hasMetaAnnotation(ThriftService.class.getName()) ||
                metadata.hasAnnotation(Component.class.getName()) || metadata.hasMetaAnnotation(Component.class.getName());
        if (contains) {
            String beanClassName = beanDefinition.getBeanClassName();
            try {
                Class<?> beanClass = Class.forName(beanClassName);
                Set<Wrapper> fieldClasses = Arrays.stream(ReflectUtil.getFields(beanClass, field -> AnnotationUtil.hasAnnotation(field, ThriftReference.class)))
                        .filter(f -> applicationContext.getBeansOfType(f.getType()).isEmpty()).map(f -> Wrapper.builder().targetClass(f.getType()).referenceClass(beanClass).element(f).elementName(f.getName()).build()).collect(Collectors.toSet());
                referenceClasses.addAll(fieldClasses);
                Arrays.stream(ReflectUtil.getMethods(beanClass, method -> AnnotationUtil.hasAnnotation(method, ThriftReference.class) && method.getParameterCount() > 0 && method.getReturnType() == Void.TYPE))
                        .map(method -> Arrays.stream(method.getParameters()).filter(p -> applicationContext.getBeansOfType(p.getType()).isEmpty()).map(p -> Wrapper.builder().targetClass(p.getType()).referenceClass(beanClass).element(method).elementName(p.getName()).build()).collect(Collectors.toSet())).collect(Collectors.toSet()).forEach(referenceClasses::addAll);
            } catch (ClassNotFoundException ignored) {
            }
        }
        return contains;
    }
}
